﻿namespace NMortgage.UI.Model
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;

    internal class MortgageCalculator
    {
        #region Fields

        private const int DAYS_IN_WEEK = 7;
        private const int MONTHS_IN_YEAR = 12;

        private List<PeriodicPayment> _payments;
        private MortgageProfile _profile;

        #endregion Fields

        #region Constructors

        public MortgageCalculator(MortgageProfile profile)
        {
            _profile = profile;
            PrincipalAmount = profile.LoanAmount;

            if (profile.PaymentFrequency == PaymentFrequency.Monthly)
            {
                _payments = SetupMonthlyPayments(profile).ToList();
            }
            else
            {
                _payments = SetupBiweeklylyPayments(profile).ToList();
            }
            Simulate();
            TotalInterestPaid = FormatCurrency(Payments.Select(x => x.InterestDue).Sum());
            TotalPayments = FormatCurrency(PrincipalAmount + TotalInterestPaid);
        }

        #endregion Constructors

        #region Properties

        public IEnumerable<PeriodicPayment> Payments
        {
            get { return _payments; }
        }

        public double PrincipalAmount
        {
            get; private set;
        }

        public double TotalInterestPaid
        {
            get; private set;
        }

        public double TotalPayments
        {
            get; private set;
        }

        #endregion Properties

        #region Methods

        private static IEnumerable<KeyValuePair<DateTime, double>> CalculateExtraPaymentSchedule(ExtraPayment extraPayment,
            DateTime mortgageStartDate, DateTime mortgageEndDate)
        {
            var payDate = mortgageStartDate.AddMonths(extraPayment.TotalOffsetInMonths);
            var lastDate = new DateTime(Math.Min(payDate.AddMonths(extraPayment.TotalDurationInMonths).Ticks, mortgageEndDate.Ticks));

            while (payDate <= lastDate)
            {
                yield return new KeyValuePair<DateTime, double>(payDate, extraPayment.Amount);

                if(!extraPayment.IsRecurring){
                    break;
                }

                switch (extraPayment.FrequencyMeasure)
                {
                    case ExtraPaymentFrequency.Days:
                        payDate = payDate.AddDays(extraPayment.Frequency);
                        break;
                    case ExtraPaymentFrequency.Weeks:
                        payDate = payDate.AddDays(extraPayment.Frequency * DAYS_IN_WEEK);
                        break;
                    case ExtraPaymentFrequency.Months:
                        payDate = payDate.AddMonths(extraPayment.Frequency);
                        break;
                    case ExtraPaymentFrequency.Years:
                        payDate = payDate.AddYears(extraPayment.Frequency);
                        break;
                    default:
                        yield break;
                }
            }
        }

        private static List<KeyValuePair<DateTime, double>> CalculateExtraPaymentSchedule(MortgageProfile profile)
        {
            var result = new List<KeyValuePair<DateTime, double>>();
            foreach (var payment in profile.ExtraPayments)
            {
                var schedule = CalculateExtraPaymentSchedule(payment, profile.StartDate, profile.EndDate);
                result.AddRange(schedule);
            }
            return result.OrderBy(x => x.Key).ToList();
        }

        private static IEnumerable<double> RemoveUntil(List<KeyValuePair<DateTime, double>> schedule, DateTime endDate)
        {
            var iterator = schedule.ToList().GetEnumerator();
            while (iterator.MoveNext())
            {
                var current = iterator.Current;
                if (current.Key < endDate)
                {
                    schedule.Remove(current);
                    yield return current.Value;
                }
                else
                {
                    break;
                }
            }
        }

        private static IEnumerable<PeriodicPayment> SetupBiweeklylyPayments(MortgageProfile profile)
        {
            StretchLastInterestRate(profile);

            var extraPaymentSchedule = CalculateExtraPaymentSchedule(profile);
            var interestCutoffDate = profile.StartDate;
            var accumulatedDate = profile.StartDate;
            foreach (var rate in profile.InterestRates)
            {
                interestCutoffDate = interestCutoffDate.AddMonths(rate.TotalDurationInMonths);
                DateTime currentDate;
                while ((currentDate = accumulatedDate.AddDays(14)) <= interestCutoffDate)
                {
                    accumulatedDate = currentDate;
                    var extraPayments = RemoveUntil(extraPaymentSchedule, accumulatedDate);
                    yield return new PeriodicPayment()
                                 {
                                     PaymentDate = accumulatedDate,
                                     InterestRate = rate.Distribution.GetRate(),
                                     ExtraPayment = extraPayments.Sum()
                                 };
                }
            }
        }

        private static IEnumerable<PeriodicPayment> SetupMonthlyPayments(MortgageProfile profile)
        {
            StretchLastInterestRate(profile);

            var extraPaymentSchedule = CalculateExtraPaymentSchedule(profile);


            var monthIndex = 0;
            foreach (var rate in profile.InterestRates)
            {
                for (int i = 0; i < rate.TotalDurationInMonths; i++)
                {
                    monthIndex++;
                    var paymentDate = profile.StartDate.AddMonths(monthIndex);
                    var interestRate = rate.Distribution.GetRate();
                    var extraPayments = RemoveUntil(extraPaymentSchedule, paymentDate);
                    yield return new PeriodicPayment()
                                 {
                                     PaymentDate = paymentDate,
                                     ExtraPayment = extraPayments.Sum(),
                                     InterestRate = interestRate
                                 };
                }
            }

        }

        private static void StretchLastInterestRate(MortgageProfile profile)
        {
            int interestMonthsCovered = profile.InterestRates.Sum(r => r.TotalDurationInMonths);
            DateTime coveredPeriod = profile.StartDate.AddMonths(interestMonthsCovered);
            int gapMonths = (int)Math.Ceiling((profile.EndDate.Subtract(coveredPeriod).Days / 28d));

            var rate = profile.InterestRates.LastOrDefault();
            int newLength = rate.TotalDurationInMonths + gapMonths;
            int newYears = newLength / 12;
            int newMonths = newLength % 12;

            rate.Years = newYears;
            rate.Months = newMonths;
        }

        /// <summary>
        ///   Calculate the periodic extraPayment using the Anuity Formula
        /// </summary>
        /// <param name="numberOfPeriods"> </param>
        /// <param name="interestRate"> </param>
        /// <param name="principalAmount"> </param>
        /// <returns> </returns>
        public static double CalculateAnnuity(int numberOfYears, double periodicRate, double principalAmount)
        {
            // Standard annuity
            return principalAmount * (periodicRate / (1 - Math.Pow((1 + periodicRate), -1 * (numberOfYears * MONTHS_IN_YEAR))));
        }

        private double FormatCurrency(double value)
        {
            var decimalDigits = CultureInfo.CurrentUICulture.NumberFormat.CurrencyDecimalDigits;
            return Math.Round(value, decimalDigits);
        }

        private void Simulate()
        {
            var numberOfPeriods = _payments.Count;
            var beginBalance = PrincipalAmount;
            var cumulativePrincipal = 0.0;
            var cumulativeInterest = 0.0;
            int frequencyFactor = ((_profile.PaymentFrequency == PaymentFrequency.Monthly) ? 1 : 2);
            int rateFactor = ((_profile.PaymentFrequency == PaymentFrequency.Monthly) ? MONTHS_IN_YEAR : 26);

            for (var i = 0; i < _payments.Count; i++)
            {
                var payment = _payments[i];
                var interestRate = payment.InterestRate;

                var standardPayment = CalculateAnnuity(_profile.TermLength, interestRate / MONTHS_IN_YEAR, PrincipalAmount);
                standardPayment = standardPayment/frequencyFactor; //weekly uses half the monthly payment amount

                var remainingPeriods = numberOfPeriods - i;

                var extraPayment = payment.ExtraPayment;
                if (extraPayment > beginBalance)
                {
                    extraPayment = beginBalance;
                    payment.ExtraPayment = extraPayment;
                }

                var netPrincipal = beginBalance - extraPayment; //remove extra payment before interest
                var interest = interestRate / rateFactor * netPrincipal;
                if (standardPayment > netPrincipal + interest)
                {
                    standardPayment = netPrincipal + interest;
                    payment.StandardPayment = standardPayment;
                }

                var principle = standardPayment - interest;
                var endBalance = netPrincipal - principle;


                cumulativePrincipal += principle + extraPayment;
                cumulativeInterest += interest;

                payment.Period = i + 1;
                payment.BeginingBalance = FormatCurrency(beginBalance);
                payment.StandardPayment = FormatCurrency(standardPayment);
                payment.EndingBalance = FormatCurrency(endBalance);
                payment.InterestDue = FormatCurrency(interest);
                payment.PrincipleDue = FormatCurrency(principle);
                payment.CumulativePrincipal = FormatCurrency(cumulativePrincipal);
                payment.CumulativeInterest = FormatCurrency(cumulativeInterest);

                beginBalance = endBalance;

                if (endBalance <= 0) { 
                    if(i<_payments.Count-1){
                        _payments.RemoveRange(i + 1, _payments.Count - 1 - i);
                    }
                    break; 
                }
            }
        }

        #endregion Methods
    }
}